﻿using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;
using static System.Windows.DragDrop;

namespace Red.Controls.Utils
{
    public interface IDragTarget : IInputElement
    {
        #region Interface

        /// <summary>
        /// Called when something is being dragged over this target
        /// </summary>
        /// <param name="currentPosition">Position of the mouse cursor</param>
        /// <param name="startPosition">Position of the mouse when the drag operation began</param>
        /// <param name="mode">Specifies the effect of this drag/drop operation</param>
        /// <param name="data">The object being dragged and it's associated user data</param>
        /// <returns></returns>
        bool OnDragOver( Point currentPosition, Point startPosition, DragDropEffects mode, DragData data );

        /// <summary>
        /// Occurs when an object is dropped within the bounds of this drag target
        /// </summary>
        /// <param name="point">Position of the mouse cursor</param>
        /// <param name="mode">Specifies the effect of this drag/drop operation</param>
        /// <param name="data">The object being dragged and it's associated user data</param>
        /// <returns>Final effect of the completed drag/drop operation</returns>
        DragDropEffects OnDrop( Point point, DragDropEffects mode, DragData data );

        /// <summary>
        /// Occurs after a drag operation is cancelled by the user
        /// </summary>
        /// <param name="mode">Specifies the effect of this drag/drop operation</param>
        /// <param name="data">The object being dragged and it's associated user data</param>
        void OnDragCancelled( DragDropEffects mode, DragData data );

        #endregion Interface

        #region Events

        /// <remarks>
        /// Implemented by <see cref="UIElement"/> and <see cref="ContentElement"/>
        /// </remarks>
        event DragEventHandler Drop;

        /// <remarks>
        /// Implemented by <see cref="UIElement"/> and <see cref="ContentElement"/>
        /// </remarks>
        event DragEventHandler DragOver;

        #endregion Events
    }

    public interface IDragSource : IInputElement
    {
        #region Interface

        /// <summary>
        /// Called to check if there is a draggable object underneath the mouse cursor
        /// </summary>
        /// <remarks>
        /// This function specifies the object that will be dragged
        /// </remarks>
        /// <param name="point">Position of the mouse cursor</param>
        /// <param name="dragObject">The potential drag object</param>
        /// <param name="userdata">The data associated with <see cref="dragObject"/></param>
        /// <returns>Type of drag drop operation valid for the dragObject. <see cref="DragDropEffects.None"/> if no drag operation can be performed</returns>
        DragDropEffects GetDragObject( Point point, out DependencyObject dragObject, out object userdata );

        /// <summary>
        /// Called at the start of a drag operation
        /// </summary>
        /// <param name="data">The object being dragged and it's associated user data</param>
        void OnDragStart( DragData data );

        /// <summary>
        /// Called at the completion of a drag operation
        /// </summary>
        /// <param name="data">The object being dragged and it's associated user data</param>
        /// <param name="mode"><see cref="DragDropEffects.None"/> if cancelled, otherwise indicates the result of the drag operation</param>
        void OnDragStop( DragData data, DragDropEffects mode );

        #endregion Interface

        #region Events

        /// <remarks>
        /// Implemented by <see cref="UIElement"/>
        /// </remarks>
        event MouseButtonEventHandler PreviewMouseDown;

        #endregion Events
    }

    public interface IDragCreateViewModel
    {
        #region Interface

        object CreateDragViewModel( Type type );

        #endregion Interface
    }

    public class DragData
    {
        #region Public Properties

        public DependencyObject DraggedObject { get; internal set; }

        public object UserData { get; set; }

        #endregion Public Properties
    }

    public class DragDrop
    {
        #region Private Types

        private enum DragState
        {
            Off,
            CheckInProgress,
            DragInProgress
        }

        #endregion Private Types

        #region Private Data

        private IDragSource _parent;
        private DependencyObject _dragObject;
        private object _dragData;

        private DragState _dragState = DragState.Off;
        private DragDropEffects _effects = DragDropEffects.None;
        private Point _dragCheckStartPosition;

        private List<IDragTarget> _targets = new List<IDragTarget>();

        #endregion Private Data

        #region C-Tor

        public DragDrop()
        {

        }

        #endregion C-Tor

        #region Public Methods

        public void Initialize( IDragSource parent )
        {
            Clear();

            _parent = parent;
            _parent.PreviewMouseDown += _parent_PreviewMouseDown;
            _parent.PreviewMouseMove += _parent_PreviewMouseMove;
        }

        public void Clear()
        {
            if( _parent != null )
            {
                _parent.PreviewMouseDown -= _parent_PreviewMouseDown;
                _parent.PreviewMouseMove -= _parent_PreviewMouseMove;

                _parent = null;
            }
        }

        public void RegisterTarget( IDragTarget target )
        {
            _targets.Add( target );

            target.DragOver += Target_DragOver;
            target.Drop += Target_Drop;
        }

        #endregion Public Methods

        #region Event Handlers

        private void _parent_PreviewMouseDown( object sender, MouseButtonEventArgs e )
        {
            if ( e.ChangedButton == MouseButton.Left )
            {
                _dragCheckStartPosition = e.GetPosition( _parent );

                _effects = _parent.GetDragObject( _dragCheckStartPosition, out _dragObject, out _dragData );

                if ( _effects != DragDropEffects.None )
                {
                    _dragState = DragState.CheckInProgress;
                }
            }
        }

        private void _parent_PreviewMouseMove( object sender, MouseEventArgs e )
        {
            if ( _dragState == DragState.CheckInProgress )
            {
                if ( e.LeftButton == MouseButtonState.Pressed )
                {
                    Point currentPosition = e.GetPosition( _parent );
                    Vector diff = _dragCheckStartPosition - currentPosition;

                    if ( Math.Abs( diff.X ) > SystemParameters.MinimumHorizontalDragDistance ||
                         Math.Abs( diff.Y ) > SystemParameters.MinimumVerticalDragDistance )
                    {
                        _dragState = DragState.DragInProgress;

                        DragData data = new DragData()
                        {
                            DraggedObject = _dragObject,
                            UserData = _dragData
                        };

                        _parent.OnDragStart( data );

                        DragDropEffects result = DoDragDrop( _dragObject, data, _effects );

                        if( result == DragDropEffects.None )
                        {
                            foreach ( IDragTarget target in _targets )
                            {
                                target.OnDragCancelled( _effects, data );
                            }
                        }

                        _parent.OnDragStop( data, result );

                        _dragObject = null;
                        _dragState = DragState.Off;
                    }
                }
                else
                {
                    _dragState = DragState.Off;
                }
            }
        }

        private void Target_DragOver( object sender, DragEventArgs e )
        {
            if( _dragState == DragState.DragInProgress )
            {
                IDragTarget target = (IDragTarget)sender;
                DragData data = (DragData)e.Data.GetData( typeof( DragData ) );
                Point point = e.GetPosition( target );

                if ( !target.OnDragOver( point, _dragCheckStartPosition, e.AllowedEffects, data ) )
                {
                    e.Effects = DragDropEffects.None;
                }
            }
        }

        private void Target_Drop( object sender, DragEventArgs e )
        {
            if ( _dragState == DragState.DragInProgress )
            {
                IDragTarget target = (IDragTarget)sender;
                DragData data = (DragData)e.Data.GetData( typeof( DragData ) );
                Point point = e.GetPosition( target );

                e.Effects = target.OnDrop( point, e.AllowedEffects, data );
            }
        }

        #endregion Event Handlers
    }
}
